/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.transas.zorlog.ui.graph;

import com.transas.zorlog.data.DataSource;
import com.transas.zorlog.data.TimeFormatterSimple;
import com.transas.zorlog.data.TimeSource;
import com.transas.zorlog.data.TimeSourceFromIndex;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.font.FontRenderContext;
import java.awt.image.BufferedImage;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JPanel;

/**
 * Graph pane prototype
 * 
 * @author amalikov
 */

public class GraphPane extends JPanel {

	public GraphPane() {
		super();
		init();
	}
	
	/**
	 * Удаляет все графики.
	 */
	public void removeAllGraph() {
		numOfBitGraphs = 0;
		graphs.clear();
		updateMinMax();
		autoColor.init();
		size = 0;
		buffer = null;
		legend = null;
		repaint();
	}
	
	/**
	 * Добавляет график в список.
	 */
	public void addGraph(DataSource ds, String name, Color color) {
		autoColor.set(color);
		addGraph(ds, name);
	}

	/**
	 * Добавляет график в список (авто цвет).
	 */
	public void addGraph(DataSource ds, String name) {
		Graph graph = new Graph(ds, name, autoColor.get());
		graphs.add(graph);
		autoColor.next();
		size = Math.max(size, ds.getSize());

		this.timesource = ds.getTimeSource();

		if (graph.isBit())
			++numOfBitGraphs;
		
		updateMinMax();

		buffer = null;
		legend = null;
		repaint();
	}

	
	/**
	 * Возвращает список графиков.
	 */
	public List<Graph> getGraphs() {
		return new ArrayList<>(graphs);
	}

	/**
	 * Возвращает список имён графиков.
	 */
	public List<String> getGraphNames() {
		List<String> names = new ArrayList<>();
		for (Graph graph : graphs)
			names.add(graph.getName());
		return names;
	}
	
	/**
	 * Устанавливает позицию и масштаб по оси времени.
	 */
	public void setOfsScale(int ofs, int scale) {
		this.scale = scale;
		this.ofs = ofs;
		buffer = null;
		repaint();
	}

	/**
	 * Изменяет позицию оси значений на заданное кол-во пикселов.
	 */
	public void changeYOfs(int y) {
		double yOfs = -y * (yMax - yMin) / (getHeight() - bottomHeaderHeight);
		
		yMax += yOfs;
		yMin += yOfs;
		
		if (yMin < yMin0) {
			yMax += yMin0 - yMin;
			yMin = yMin0;
		}

		if (yMax > yMax0) {
			yMin -= yMax - yMax0;
			yMax = yMax0;
		}
		
		buffer = null;
		repaint();
	}

	/**
	 * Изменяет масштаб по оси значений на заданное кол-во шагов, сохраняя позицию относительно курсора.
	 */
	public void changeYScale(int step, int y) {
		double value = yMax - y * (yMax - yMin) / (getHeight() - bottomHeaderHeight);
		double scaleStep = 0.1;
		double center = (yMin + yMax) / 2;
		double range = (yMax - yMin) * (1 + step * scaleStep);
		
		yMin = Math.max(center - range / 2, yMin0);
		yMax = Math.min(center + range / 2, yMax0);
		
		int y1 = (int)((yMax - value) / (yMax - yMin) * (getHeight() - bottomHeaderHeight));
		
		changeYOfs(y1 - y);
	}
	
	
	/**
	 * Устанавливает позицию курсора.
	 */
	public void setCursorPosition(int cursor) {
		this.cursor = cursor;
		repaint();
	}
	
	
	/**
	 * Возвращает ширину поля вывода графиков.
	 */
	public int getGraphWidth() {
		return getWidth() - leftHeaderWidth;
	}

	/**
	 * Преобразует координаты относительно поля графика.
	 * @param x координата относительно компонента
	 * @return координата относительно поля графика
	 */
	public int getGraphX(int x) {
		return Math.max(x - leftHeaderWidth, 0);
	}
	
	/**
	 * Проверяет координату на попадание в поле вертикальной оси.
	 * @param x координата относительно компонента
	 * @return true если в поле вертикальной оси
	 */
	public boolean isOnVerticalHeader(int x) {
		return x < leftHeaderWidth;
	}

	
	/**
	 * Рисует компонент, используя буферизацию.
	 */
	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		if (buffer == null)
			rebuildBuffer();
		
		g.drawImage(buffer, 0, 0, this);

		paintCursor(g);
		
		if (legend == null)
			legend = Legend.create(legendType, legendAlignX, legendFont, graphs);

		legend.paint(getAreaGraphics(g, Area.WORK));
	}

	
	private enum Area {
		WORK, BOTTOMHEADER, LEFTHEADER 
	}
	
	private Graphics getAreaGraphics(Graphics orig, Area area) {
		switch (area) {
			case WORK:
				return orig.create(leftHeaderWidth, 0, getBounds().width - leftHeaderWidth, getBounds().height - bottomHeaderHeight);
			case BOTTOMHEADER:
				return orig.create(leftHeaderWidth, getBounds().height - bottomHeaderHeight, getBounds().width - leftHeaderWidth, bottomHeaderHeight);
			case LEFTHEADER:
				return orig.create(0, 0, leftHeaderWidth, getBounds().height);
			default:
				return orig;
		}
	}
	
	
	/**
	 * Добавляет слушатели для отслеживания изменения размера компонента и перерисовки буфера.
	 */
	private void init() {
		addComponentListener(new ComponentAdapter() {
			private Dimension lastSize = null;

			@Override
			public void componentShown(ComponentEvent e) {
				buffer = null;
				lastSize = getSize();
			}

			@Override
			public void componentResized(ComponentEvent e) {
				if (!getSize().equals(lastSize)) {
					buffer = null;
					lastSize = getSize();
					repaint();
				}
			}
		});
	}
	
	
	/**
	 * Перерисовывает буфер. В буфер попадает всё кроме курсора и легенды.
	 */
	private void rebuildBuffer() {
		buffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = buffer.createGraphics();
		
		paintBackground(g);
		updateTickSteps();
		paintXTicks(g);
		
		if (isAnyAnalogGraph())
			paintYTicks(g);
		
		paintGraphs(getAreaGraphics(g, Area.WORK));
	}
	
	
	/**
	 * Рисует фон.
	 */
	private void paintBackground(Graphics g) {
		Rectangle rect = getBounds();
		g.setColor(getBackground());
		g.fillRect(leftHeaderWidth, 0, rect.width, rect.height);
		g.setColor(headerColor);
		g.fillRect(0, 0, leftHeaderWidth, rect.height);
		g.fillRect(0, rect.height - bottomHeaderHeight, rect.width, bottomHeaderHeight);
	}

	
	/**
	 * Рисует вертикальные метки.
	 */
	private void paintXTicks(Graphics g) {
		Graphics work = getAreaGraphics(g, Area.WORK);
		Graphics bottomHeader = getAreaGraphics(g, Area.BOTTOMHEADER);
		
		double xmin = timesource.getValue(ofs);
		double xmax = timesource.getValue(ofs + size / (1 << scale));
		
		if (xmax == xmin)
			return;
		
		Rectangle rect = work.getClipBounds();
		double xscale = rect.width / (xmax - xmin);
		bottomHeader.setFont(ticksFont);
		
		for (double x = xTick0; x < xmax; x += xTickStep) {
			int xi = (int)((x - xmin) * xscale + 0.5);
			work.setColor(tickLinesColor);
			work.drawLine(xi, 0, xi, rect.height);
			String str = timesource.getTimeFormatter().get(x);
			bottomHeader.setColor(Color.BLACK);
			bottomHeader.drawString(str, xi - (int)(g.getFontMetrics().getStringBounds(str, g).getWidth() / 2), ticksFont.getSize() + 2);
		}
	}
	

	/**
	 * Рисует горизонтальные метки.
	 */
	private void paintYTicks(Graphics g) {
		Graphics work = getAreaGraphics(g, Area.WORK);
		Graphics leftHeader = getAreaGraphics(g, Area.LEFTHEADER);
		
		Rectangle rect = work.getClipBounds();
		double yscale = rect.height / (yMax - yMin);
		
		leftHeader.setFont(ticksFont);
		
		NumberFormat nf = NumberFormat.getNumberInstance();
		nf.setMinimumFractionDigits(0);
		
		for (double y = yTick0; y < yMax; y += yTickStep) {
			int yi = rect.height - (int)((y - yMin) * yscale + 0.5);
			work.setColor(tickLinesColor);
			work.drawLine(0, yi, rect.width, yi);
			
			String str = nf.format(y);
			int x = Math.max(0, leftHeader.getClipBounds().width - 4 - (int)(g.getFontMetrics().getStringBounds(str, g).getWidth()));
			
			leftHeader.setColor(Color.BLACK);
			leftHeader.drawString(str, x, yi + ticksFont.getSize() / 2);
		}
	}


	/**
	 * Рисует сами графики.
	 */
	private void paintGraphs(Graphics g) {
		Rectangle rect = g.getClipBounds();
		
		int begin = ofs;
		int end = ofs + size / (1 << scale);
		
		if (end > size)
			end = size;
		
		int indexOfBitGraph = 0;
		for (Graph graph : graphs) {
			if (graph.isBit()) {
				graph.setYOffset((indexOfBitGraph + 1) * rect.height / (numOfBitGraphs + 1));
				++indexOfBitGraph;
			}

			graph.draw(g, rect, yMin, yMax, begin, end);
		}
	}
	

	/**
	 * Поиск общего минимума и максимума.
	 */
	private void updateMinMax() {
		boolean first = true;		
		yMin0 = yMax0 = 0;
		
		for (Graph graph : graphs) {
			if (graph == null)
				continue;
			
			if (first) {
				yMin0 = graph.getMin();
				yMax0 = graph.getMax();
				first = false;
			} else {
				yMin0 = Math.min(yMin0, graph.getMin());
				yMax0 = Math.max(yMax0, graph.getMax());
			}
		}

		double gap = (yMax0 - yMin0) * 0.01;
		
		yMax0 += gap;
		yMin0 -= gap;
		
		yMax = yMax0;
		yMin = yMin0;
	}

	
	/**
	 * Определение масштаба меток по осям X и Y.
	 */
	private void updateTickSteps() {
		int w = (int)ticksFont.getMaxCharBounds(new FontRenderContext(null, false, false)).getWidth() * 4;
		int nx = (getBounds().width - leftHeaderWidth) / w;		
		
		double xmin = timesource.getValue(ofs);
		double xmax = timesource.getValue(ofs + size / (1 << scale));
		xTickStep = getTickStep(xmin, xmax, nx, new double[]{1, 1.5, 3, 6});
		xTick0 = xTickStep * (int)(xmin / xTickStep + 1);
		
		int ny = (getBounds().height - bottomHeaderHeight) / (ticksFont.getSize() * 2);
		yTickStep = getTickStep(yMin, yMax, ny, new double[]{1, 2, 5});
		yTick0 = yTickStep * (int)(yMin / yTickStep + (yMin > 0 ? 1.5 : -0.5));
		
		if (yTick0 < yMin)
			yTick0 += yTickStep;		
	}
	
	
	/**
	 * Определение масштаба меток из заданного ряда.
	 */
	private double getTickStep(double min, double max, int n, double[] mlist) {
		double step0 = (max - min) / n;
		double step1 = Math.pow(10, Math.floor(Math.log10(step0)));
		double mindelta = step0;
		double res = step1;
		
		for (double m : mlist) {
			double delta = Math.abs(step0 - m * step1);
			
			if (delta < mindelta) {
				mindelta = delta;
				res = m * step1;
			}
		}
		
		return res != 0 ? res : 1;
	}
	
	
	/**
	 * Рисует вертикальный курсор.
	 */
	private void paintCursor(Graphics g) {
		if (cursor < ofs || cursor >= ofs + (size / (1 << scale)) || cursor >= size)
			return;

		g.setFont(valuesFont);		
		
		Graphics work = getAreaGraphics(g, Area.WORK);
		Graphics bottomHeader = getAreaGraphics(g, Area.BOTTOMHEADER);
		
		Rectangle rect = work.getClipBounds();
		double xscale = rect.width / (double)(size / (1 << scale));
		int x = (int)((cursor - ofs) * xscale);
		
		g.setColor(Color.BLACK);
		work.drawLine(x, 0, x, rect.height - 1);
		
		NumberFormat nf = NumberFormat.getNumberInstance();
		nf.setMinimumFractionDigits(0);		
		
		for (Graph graph : graphs) {
			double value = graph.getValue(cursor);
			String strvalue = nf.format(value);
			int y = 0;
			
			if (graph.isBit()) {
				y = graph.getYOffset();
				strvalue = value != 0 ? graph.getName() : null;
			} else {
				double yscale = rect.height / (yMax - yMin);
				y = (int)((yMax - value) * yscale + 0.5);
			}
			
			paintBorderedText(work, graph.getColor(), x, y - valuesFont.getSize() / 2, strvalue);
		}
		
		paintBorderedText(bottomHeader, Color.BLACK, x, 0, timesource.getValueAsString(cursor));
	}

	/**
	 * Рисует текст в рамке
	 */
	private void paintBorderedText(Graphics g, Color color, int x, int y, String valueStr) {
		if (valueStr == null)
			return;
		
		Rectangle rect = g.getClipBounds();
		
		int h = g.getFont().getSize();
		int w = (int)g.getFontMetrics().getStringBounds(valueStr, g).getWidth() + 4;
		
		if (x + w > rect.width)
			x = rect.width - w;
		
		if (y < 0)
			y = 0;
		else if (y + h > rect.height)
			y = rect.height - h;
		
		g.setColor(new Color(0xC0FFFFFF, true));
		g.fillRect(x, y, w, h);
		g.setColor(Color.BLACK);
		g.drawRect(x, y, w, h);

		g.setColor(color);
		g.drawString(valueStr, x + 2, y + h - 2);
	}
	
	/**
	 * Проверяет наличие "аналоговых" графиков
	 */
	private boolean isAnyAnalogGraph() {
		for (Graph graph: graphs)
			if (!graph.isBit())
				return true;
				
		return false;
	}
			
	private BufferedImage buffer = null;
	private Legend legend = null;
	
	private final int leftHeaderWidth = 40;
	private final int bottomHeaderHeight = 20;
	private final Font ticksFont = new Font("Arial", Font.BOLD, 12);
	private final Font valuesFont = new Font("Arial", Font.PLAIN, 10);
	private final Font legendFont = new Font("Arial", Font.BOLD, 12);
	private final LegendType legendType = LegendType.VERT;
	private final LegendAlignX legendAlignX = LegendAlignX.RIGHT;
	private final Color headerColor = new Color(0xF0F0D0);
	private final Color tickLinesColor = new Color(0xC0C0C0);
	
	private List<Graph> graphs = new ArrayList<>();
	private TimeSource timesource = new TimeSourceFromIndex(1, new TimeFormatterSimple());

	private int size = 0;
	private int ofs = 0;
	private int scale = 0;
	private int cursor = 0;

	private AutoColor autoColor = new AutoColor(0, 0.77f, 1, 0.95f);

	private double xTick0 = 0;
	private double xTickStep = 1;

	private double yMin0 = 0;
	private double yMax0 = 0;
	private double yMin = 0;
	private double yMax = 0;
	private double yTick0 = 0;
	private double yTickStep = 1;
	
	private int numOfBitGraphs = 0;
}

